import numpy as np  # 导入所需的numpy库

# 期货策略模版 - 经典强盗布林策略

# 经典强盗布林策略:
# 1、入场交易规则（日线）价格超过 50 日移动平均线上方 1 个标准差作为买进信号的标准，跌破 50 日移动平均线下方 1 个标准差作为卖出信号的标准（主体交易条件）。当日收盘价必须高于 30 日前的收盘价才能做多，当日收盘价必须低于 30 日前的收盘价才能做空。这个额外的要求是一个趋势过滤器。我们只希望在上升趋势中做多或者在下降趋势中做空。
# 2、出场交易规则（日线）我们都知道常规 3 条线组成的通道策略，都是以突破上下轨开仓，回归中轨平仓。但是这种平仓方式，将舍弃很大一块利润。布林强盗交易系统采用了一个较另类、较积极的方式（主条件）：当建立仓位时，保护性止损设置在 50 日均线（中轨）。之后持有头寸的时间每多一天，计算移动平均线的天数减一。持有头寸时间越长，我们越容易带着利润离场。计算移动平均线的天数最小可以递减到 10，如果达到 10，则不再递减。

#  国内期货（15分钟）修改后策略逻辑入场条件：价格突破布林带上轨（75 日移动平均线 3 个标准差），收盘价大于90 周期收盘价最高值，即做多；价格跌破布林带下轨（75 日移动平均线 3 个标准差），收盘价小于90 周期收盘价最低值，即做空。出场条件：持多仓的情况下，收盘价小于出场MA平仓；持空仓的情况下，收盘价大于出场MA平仓；


# 初始化函数,全局只运行一次
def init(context):
    # 设立商品期货子账户
    set_subportfolios([{"cash": 0, "type": 'stock'}, {"cash": 100000, "type": "future"}])
    log.info('策略开始运行,初始化函数全局只运行一次')
    
    # 设置期货手续费和滑点
    set_commission(PerShare(type='future', cost=0.000045))
    set_slippage(PriceSlippage(0.005), 'future')
    
    # 设置螺纹钢保证金比例
    set_margin_rate('RB', 0.08, 0.09)
    
    # 设置成交量限制
    set_volume_limit(0.25, 0.5)
    
    # 设置策略参数
    context.ins = 'RB9999'         # 交易标的
    context.boll_period = 75       # 布林带周期
    context.boll_dev = 3.0         # 标准差倍数
    context.filter_period = 90     # 趋势过滤周期
    context.ma_init = 44           # 初始均线周期
    context.ma_min = 18            # 最小均线周期
    context.entry_bar_index = {}    # 记录开仓时的bar索引
    context.bar_count = 0          # 初始化bar计数器
    
    # 设置基准和订阅品种
    set_benchmark(context.ins)
    subscribe(context.ins)
    log.info(f'策略参数初始化完成：布林周期={context.boll_period}，标准差倍数={context.boll_dev}，过滤周期={context.filter_period}')

# 每日开盘前运行
def before_trading(context):
    date = get_datetime().strftime('%Y-%m-%d %H:%M:%S')
    log.info('{} 盘前运行'.format(date))

# 核心交易逻辑（15分钟级别）
def handle_bar(context, bar_dict):
    time = get_datetime().strftime('%Y%m%d %H%M')
    log.info('{} 盘中运行'.format(time))
    
    # 获取合约代码和当前价格
    ins = context.ins
    current_close = bar_dict[ins].close
    current_open = bar_dict[ins].open
    
    # 获取历史数据用于指标计算
    price_fields = ['close']
    max_bars = max(context.boll_period, context.filter_period) + 1
    prices = get_price_future(ins, None, time, '15m', price_fields, bar_count=max_bars)
    
    # 检查数据是否足够
    if len(prices) < max_bars:
        log.info(f'历史数据不足({len(prices)}<{max_bars})，跳过当前bar')
        return
    
    # 提取收盘价序列
    closes = prices['close'].values
    
    # 计算布林带指标
    closes_75 = closes[-context.boll_period:]
    ma_75 = closes_75.mean()
    std = closes_75.std()
    upper = ma_75 + context.boll_dev * std
    lower = ma_75 - context.boll_dev * std
    
    # 计算90周期最高/最低收盘价（趋势过滤器）
    closes_90 = closes[-context.filter_period:]
    highest_close_90 = closes_90.max()
    lowest_close_90 = closes_90.min()
    
    # 获取当前持仓
    future_account = context.portfolio.future_account
    long_amount = future_account.positions[ins].long_amount
    short_amount = future_account.positions[ins].short_amount
    position = 1 if long_amount > 0 else (-1 if short_amount > 0 else 0)
    
    # ===== 动态移动止损计算 =====
    dyn_ma = None
    if position != 0 and ins in context.entry_bar_index:
        # 计算持仓周期数
        holding_bars = context.bar_count - context.entry_bar_index[ins]
        
        # 计算动态均线周期
        dyn_period = context.ma_init - holding_bars
        dyn_period = max(dyn_period, context.ma_min)
        
        # 获取所需的历史开盘价数据
        open_bars = dyn_period  # 需要获取的K线数量
        open_data = get_price_future(ins, None, time, '15m', ['open'], bar_count=open_bars)['open'].values
        
        # 计算动态均线（使用开盘价）
        dyn_ma = np.mean(open_data)
        log.info(f'动态移动止损: 持仓{holding_bars}根K线, 周期={dyn_period}, 止损价={dyn_ma:.2f}')
    
    # ===== 交易信号判断 =====
    # 1. 多头开仓条件
    if position == 0:
        if current_close > upper and current_close > highest_close_90:
            # 开多
            order_future(ins, 20, 'open', 'long', None)
            context.entry_bar_index[ins] = context.bar_count
            log.info(f'满足开多条件: 收盘价{current_close:.2f} > 布林上轨{upper:.2f} '
                     f'且 > 90周期最高价{highest_close_90:.2f}')
    
    # 2. 空头开仓条件
    if position == 0:
        if current_close < lower and current_close < lowest_close_90:
            # 开空
            order_future(ins, 20, 'open', 'short', None)
            context.entry_bar_index[ins] = context.bar_count
            log.info(f'满足开空条件: 收盘价{current_close:.2f} < 布林下轨{lower:.2f} '
                     f'且 < 90周期最低价{lowest_close_90:.2f}')
    
    # 3. 多单平仓条件
    if long_amount > 0 and dyn_ma is not None:
        if current_close < dyn_ma:
            order_future(ins, long_amount, 'close', 'long', None)
            if ins in context.entry_bar_index:
                del context.entry_bar_index[ins]
            log.info(f'多单平仓: 收盘价{current_close:.2f} < 动态均线{dyn_ma:.2f}')
    
    # 4. 空单平仓条件
    if short_amount > 0 and dyn_ma is not None:
        if current_close > dyn_ma:
            order_future(ins, short_amount, 'close', 'short', None)
            if ins in context.entry_bar_index:
                del context.entry_bar_index[ins]
            log.info(f'空单平仓: 收盘价{current_close:.2f} > 动态均线{dyn_ma:.2f}')
    
    # 更新bar计数器
    context.bar_count += 1

# 收盘后运行
def after_trading(context):
    time = get_datetime().strftime('%Y-%m-%d %H:%M:%S')
    log.info('{} 盘后运行'.format(time))
    log.info('一天结束')